Skip to content

API proposal for NavigateTo and NavLink with relative path#64670

Merged
ilonatommy merged 17 commits intodotnet:mainfrom
ilonatommy:fix-23615
Jan 14, 2026
Merged

API proposal for NavigateTo and NavLink with relative path#64670
ilonatommy merged 17 commits intodotnet:mainfrom
ilonatommy:fix-23615

Conversation

@ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented Dec 5, 2025

Summary

This change implements the RelativeToCurrentUri parameter for Blazor's NavigationManager.NavigateTo() and NavLink component, allowing developers to navigate to URIs relative to the current page path rather than the application's base URI.

Background

You have a Blazor app with nested folder structure like a file explorer or documentation site:

/docs/
/docs/getting-started/
/docs/getting-started/installation.html
/docs/getting-started/configuration.html

When you're at /docs/getting-started/installation.html and want to navigate to a sibling page:

NavigationManager.NavigateTo("configuration.html");  

the navigation redirects to /configuration.html (app root), instead of the expected /docs/getting-started/configuration.html

The same limitation affected NavLink components.

Fixes #23615

Changes

Public API

namespace Microsoft.AspNetCore.Components;

public partial struct NavigationOptions
{
    public bool RelativeToCurrentUri { get; init; }
}

public class NavLink : ComponentBase
{
    [Parameter] public bool RelativeToCurrentUri { get; set; }
}

Implementation Details

NavigationManager: When RelativeToCurrentUri is true, NavigateTo() calls ResolveRelativeToCurrentPath() to compute the absolute URI using zero-allocation span-based operations before passing it to NavigateToCore().

NavLink: When RelativeToCurrentUri is true, OnParametersSet() resolves the relative href server-side during rendering, ensuring the anchor tag contains the correct absolute path for SSR scenarios.

Performance: The implementation avoids creating Uri objects, using only ReadOnlySpan<char> operations and string.Concat() for efficiency.

Tests

Added 7 NavigationManager tests and 6 NavLink tests covering path-relative navigation, query/fragment handling, nested paths, and edge cases.

Usage

NavigationManager:

// Navigate to a sibling page
NavigationManager.NavigateTo("details.html", new NavigationOptions { RelativeToCurrentUri = true });
@inject NavigationManager NavManager

<button @onclick='() => NavManager.NavigateTo("details.html", new NavigationOptions { RelativeToCurrentUri = true })'>
    View Details
</button>

NavLink:

<NavLink href="details" RelativeToCurrentUri="true">View Details</NavLink>

@ilonatommy ilonatommy added this to the .NET 11 Planning milestone Dec 5, 2025
@ilonatommy ilonatommy self-assigned this Dec 5, 2025
Copilot AI review requested due to automatic review settings December 5, 2025 15:27
@ilonatommy ilonatommy requested a review from a team as a code owner December 5, 2025 15:27
@ilonatommy ilonatommy added the area-blazor Includes: Blazor, Razor Components label Dec 5, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a PathRelative navigation option to Blazor's NavigationManager, enabling developers to navigate to URIs relative to the current page's directory path rather than the application's base URI. This addresses a common pain point where navigating to sibling pages required manual path resolution.

Key Changes:

  • Adds NavigationOptions.PathRelative property for path-relative navigation
  • Implements ResolveRelativeToCurrentPath method using zero-allocation span-based operations
  • Adds TypeScript interface definition for JavaScript interop
  • Includes 7 unit tests covering basic scenarios and edge cases

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Components/Components/src/NavigationOptions.cs Adds PathRelative boolean property to NavigationOptions struct with XML documentation
src/Components/Components/src/NavigationManager.cs Implements path-relative resolution logic in NavigateTo and new internal ResolveRelativeToCurrentPath method
src/Components/Components/src/PublicAPI.Unshipped.txt Registers the new public API property and its init accessor
src/Components/Components/test/NavigationManagerTest.cs Adds unit tests for path-relative navigation scenarios and helper class for tracking navigations
src/Components/Web.JS/src/Services/NavigationManager.ts Adds optional pathRelative field to TypeScript NavigationOptions interface

@javiercn
Copy link
Member

javiercn commented Dec 5, 2025

I don't think we should take this change.

This change implements the PathRelative parameter for Blazor's NavigationManager.NavigateTo() and NavLink component, allowing developers to navigate to URIs relative to the current page path rather than the application's base URI.

This is not a thing in the web. The original issue refers to users hosting inside MVC and not adding a base to the document, which causes relative links to be resolved relative to the current page.

This change doesn't address that and introduces a way of composing a path that doesn't make sense. There is a way of creating relative URLs which is using ../ to traverse upwards and ./ to traverse from the current URL.

Absolute URL means a complete URI with scheme and authority (for example https://example.com/page) and is unambiguous. Absolute‑path references (often called “root‑relative” in casual usage) begin with / and are not full URIs by themselves; they are resolved to an absolute URL using the page’s origin or the document base. Relative‑path references (no leading slash) are resolved against the document’s directory or the <base> href.

  • Absolute URL — includes scheme and host; independent of document location (e.g., https://cdn.example.com/a.png).
  • Absolute‑path reference — begins with /; describes a path from the origin root but lacks scheme/authority until resolved. People often call this “root‑relative,” but technically it’s a relative reference with an absolute‑path form.
  • Relative‑path reference — a path like images/pic.jpg or ../page.html resolved relative to the document’s path.
    These distinctions matter because the path component of the document URL (its directory) is what the browser uses to resolve non‑leading‑slash relative references.

When a browser sees a non‑absolute string it runs the URL resolution algorithm: if a <base href> exists, that base is used; otherwise the document’s URL (its origin and path) is used. A relative path without / is appended to the document directory; a leading / replaces the path with the absolute path on the base/origin before resolving to a full URL. The URL API and browser implementations follow this algorithm to produce the final absolute URL.

Relationship of <base> to the path

The <base href="..."> overrides the document location for resolving all relative references (images, scripts, anchors, styles) so that /foo and foo are interpreted relative to that base’s origin and path rather than the page file’s URL. That means adding <base> can change how / maps to an origin and can make previously working resource paths break if not adjusted.


Practical guidance and gotchas

  • Use full absolute URLs for external resources and when you need an origin‑independent link.
  • Use relative or absolute‑path references for internal links to keep portability, but audit when adding <base> because it affects every relative reference on the page.
  • Test after adding <base>: scripts, CSS, and images can silently fail if their resolved paths change.

@ilonatommy
Copy link
Member Author

ilonatommy commented Dec 5, 2025

Let's discuss it a bit more.

This is not a thing in the web.

I was inspired by this API: https://angular.dev/guide/routing/navigate-to-routes#routernavigate. Blazor would not be a pioneer in allowing relativePath param.

The original issue refers to users hosting inside MVC and not adding a base to the document, which causes relative links to be resolved relative to the current page.

I don't believe so, the original issue is about blazor treating nested paths not as dirs but like a route, so not recognizing that in /a/b/c -> c is a file in b, as browser would but treating it whole a/b/c as "file name/route" under the base. The original request stands for both: base: "/" and base: "/a". In both situations we would like to have a way to navigate to /a/b/d from /a/b/c, passing just d to the navigation component/method.

What do you think?

@ilonatommy ilonatommy changed the title API proposal for NavigateTo with relative path API proposal for NavigateTo and NavLink with relative path Dec 5, 2025
@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Dec 12, 2025
@ilonatommy ilonatommy removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jan 5, 2026
@ilonatommy
Copy link
Member Author

What do you think?

For the future reviewers: this conversation is resolved. We agreed the feature is really missing.

Copy link
Member

@pavelsavara pavelsavara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to also use NavigationManager.NavigateTo("./configuration.html"); and NavigationManager.NavigateTo("../../configuration.html"); to express relative path, instead of passing the option.

@ilonatommy
Copy link
Member Author

I would like to also use NavigationManager.NavigateTo("./configuration.html"); and NavigationManager.NavigateTo("../../configuration.html"); to express relative path, instead of passing the option.

That's a justified ask but ./ and ../../ already have their meaning and are resolved against the BaseUri (not current uri). It would be a breaking change that I would prefer to avoid. My goal in this PR was to satisfy the community requirements without breaking existing customers. We could discuss it more if you feel this risk should be taken.

@pavelsavara
Copy link
Member

I would like to also use NavigationManager.NavigateTo("./configuration.html"); and NavigationManager.NavigateTo("../../configuration.html"); to express relative path, instead of passing the option.

That's a justified ask but ./ and ../../ already have their meaning and are resolved against the BaseUri (not current uri). It would be a breaking change that I would prefer to avoid. My goal in this PR was to satisfy the community requirements without breaking existing customers. We could discuss it more if you feel this risk should be taken.

I'm naive user of this API. I would not know/notice that it could sometimes refer to baseURI and sometimes to absolute root. I would have hard time to discover the optional option parameter. Does PathRelative in fact switch that ? If so, should it have different name ?

What would NavigationManager.NavigateTo("../../details.html", new NavigationOptions { PathRelative = true }); do ?

@ilonatommy
Copy link
Member Author

ilonatommy commented Jan 7, 2026

I'm naive user of this API. I would not know/notice that it could sometimes refer to baseURI and sometimes to absolute root. I would have hard time to discover the optional option parameter. Does PathRelative in fact switch that ? If so, should it have different name ?

What would NavigationManager.NavigateTo("../../details.html", new NavigationOptions { PathRelative = true }); do ?

Let's do it by an example. We have an application with base tag set to /app/admin/. The NavigateTo call happens on /app/admin/settings/password/change.html and tries to navigate to ../general. Scenarios:

  • PathRelative = true, so NavigationManager.NavigateTo("../general.html", new NavigationOptions { PathRelative = true }). The path is resolved as relative to the current location.
    Result -> /app/admin/settings/general.html (/app/admin/setting/password was a directory for change.html file, so we go one level up from that dir).

  • PathRelative = false, so NavigationManager.NavigateTo("../general.html"). The path is resolved as relative to the base.
    Result -> /app/general.html (because base uri was /app/admin/ and we went one level up).

Is it still confusing?

@javiercn
Copy link
Member

javiercn commented Jan 7, 2026

@pavelsavara what this API does is offer a convenience method to generate document relative urls independent of the existence of a base tag. Below is a more detailed explanation

Without a <base> tag:

  • Absolute URLs (/path) - resolve from domain root
  • Relative URLs (../Details, ./Edit) - resolve relative to the current document's path

With a <base> tag:

  • Absolute URLs (/path) - still resolve from domain root (unaffected)
  • Relative URLs (../Details, ./Edit) - resolve relative to the <base> href, not the current document

This is standard browser behavior, and changing it would be a breaking change.

Resolution Examples

Current URL: https://example.com/base/Customer/5/Details/ with <base href="/base/">:

Link Without <base> With <base href="/base/">
/products/ /products /products
Edit/ /base/Customer/5/Edit/ /base/Edit/
./Edit/ /base/Customer/5/Edit/ /base/Edit/
../Details/ /base/Customer/5/Details/ /Details/
../../Orders/ /base/Orders/ /Orders/

ASP.NET Core MVC's URL generation APIs (Url.Action(), Url.RouteUrl(), LinkGenerator, <a asp-action="">) always produce absolute URLs (starting with /), which are unaffected by the <base> tag. This pattern is used heavily across MVC apps in views, controllers, and middleware.

The option switches resolution from base-relative to document-relative, resolving as if there were no <base> tag.

Current URL: https://example.com/base/Customer/5/Details with <base href="/base/"> so "../Details/", new NavigationOptions { PathRelative = true } becomes base/Customer/5/Details/ instead of /Details/.

The goal is to explicitly enable document-relative resolution when needed, without breaking existing code.

@pavelsavara
Copy link
Member

The goal is to explicitly enable document-relative resolution when needed, without breaking existing code.

How does the migration process look like ? With this API they would have to add PathRelative=true to every link. Could we flip that switch on a page level ? Should we have separate methods which always do that so the legacy code could be migrated to that ?

@ilonatommy
Copy link
Member Author

ilonatommy commented Jan 7, 2026

How does the migration process look like ? With this API they would have to add PathRelative=true to every link. Could we flip that switch on a page level ? Should we have separate methods which always do that so the legacy code could be migrated to that ?

Because currently they are using absolute paths, there's no magic way to migrate. Users would have to rename each place with NavigateTo/NavLink from /my/absolute/path/**interesting-relative-part** to **interesting-relative-part**, they can add a regex to their rename that passes the switch to NavigationOptions. Switch variable can be saved per page and reused, like any other variable.

@javiercn
Copy link
Member

javiercn commented Jan 7, 2026

How does the migration process look like ? With this API they would have to add PathRelative=true to every link. Could we flip that switch on a page level ? Should we have separate methods which always do that so the legacy code could be migrated to that ?

No. The right level to do this is to provide an option on the API in navigation manager. Most people don't use or need this. It's a convenience API for the few people that have requested it. They can already achieve this themselves.

Anything bigger than offering an option on the existing navigation manager API and maybe something in NavLink is out of the scope and will require a proper design document and justification for the work.

Let's not turn a simple enhancement into a redesign of an area.

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@ilonatommy ilonatommy requested a review from javiercn January 14, 2026 14:12
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@ilonatommy ilonatommy enabled auto-merge (squash) January 14, 2026 14:28
@ilonatommy ilonatommy merged commit 3429b12 into dotnet:main Jan 14, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blazor Relative URLs in components / Sub-navigation issues

3 participants